組込み現場の「C++」プログラミング 明日から使える徹底入門

高木 信尚(株式会社クローバーフィールド

3.6 new演算子とdelete演算子の裏側

new演算子とdelete演算子といえば,動的にオブジェクトを生成/解体するためのものですが,malloc関数とfree関数の代わり程度にしか理解されていないこともよくあります.実際には,new演算子とdelete演算子は,malloc関数とfree関数より,ずっと多くの機能を持っており,それゆえに複雑な振る舞いをします.

3.6.1 new演算子とdelete演算子の振る舞い

まずは,C++が標準的にサポートしているnew演算子とdelete演算子をリストアップしてみましょう(見た目を簡単にするために,std::は省略しています).

void* operator new(size_t size) throw(bad_alloc);
void* operator new(size_t size,
                     const nothrow_t&) throw();
void* operator new(size_t size, void* ptr) throw();
void operator delete(void* ptr) throw();
void operator delete(void* ptr,
                     const nothrow_t&) throw();
void operator delete(void* ptr, void*) throw();

void* operator new[](size_t size) throw(bad_alloc);
void* operator new[](size_t size,
                     const nothrow_t&) throw();
void* operator new[](size_t size, void* ptr) throw();
void operator delete[](void* ptr) throw();void operator delete[](void* ptr,
                       const nothrow_t&) throw();
void operator delete[](void* ptr, void*) throw();

たくさんの宣言が並んでいますが,大きく分けると,前半は単一のオブジェクトを生成するためのものであり,後半は配列を生成するためのものです.以下の説明では,特に断りがないかぎり,単一のオブジェクトを生成するものだけに触れますが,配列を生成するものについても同じことがいえます.

new演算子とdelete演算子は,必ず対のものが用意されています.どれとどれが対になっているかというと,次のようになります.

【最もシンプルな形式】

void* operator new(size_t size) throw(bad_alloc);
void operator delete(void* ptr) throw();

【例外を送出しない形式】

void* operator new(size_t size,
                   const nothrow_t&) throw();
void operator delete(void* ptr,
                     const nothrow_t&) throw();

【位置指定形式】

void* operator new(size_t size, void* ptr) throw();
void operator delete(void* ptr, void*) throw();

ソースコードの中で,delete object;と記述すれば,最もシンプルな形式のdelete演算子が呼び出されます.ではなぜ,それぞれの演算子に対応するdelete演算子が存在するのでしょうか? それは,new演算子の振る舞いを詳しく見れば明らかになります.

Type * ptr = new Type(arg);
  ↓
// new演算子の振る舞いを示す擬似コード
void* __temp = operator new(sizeof(Type));
Type* ptr = (Type*)(__temp);
if (ptr != 0)
{
    try
    {
        // ← コンストラクタを呼び出す.実際にはこのような書き方はできない 
        ptr->Type(arg);
    }
    catch (...)
    {
        operator delete(ptr);
        throw;
    }
}

これは,new演算子を使ってオブジェクトを生成しようとしたとき,実際にどのようなことが行われているかを示すための擬似コードです.まずはじめに,operator new関数(newを演算子ではなく関数として使用したもので,コンストラクタは呼ばれません)によってメモリブロックが割り付けられます.メモリブロックの割り付けに成功すれば,次に生成しようとする型のコンストラクタが(もしあれば)呼び出されます.

ここで,コンストラクタから例外が送出される可能性があるので,もし例外が送出された場合には,new演算子と対になるoperator delete関数(deleteを演算子ではなく関数として使用したもので,デストラクタは呼ばれません)が呼び出されます.このとき,オブジェクトはまだ生成されていませんので,デストラクタは呼び出されません.最後に,例外を再送出します.

先ほどの擬似コードは,最もシンプルな形式のnew演算子の振る舞いに関するものですが,他の形式でも同じことがいえます.ただし,コンストラクタから例外が送出された場合に呼び出されるoperator delete関数は,operator new関数と対になるものが使用されます.もし,対になるoperator delete関数が定義されていなければ,コンパイルエラーにはならず,単に解放処理が行われません.結果として,operator new関数で割り付けられたメモリブロックは解放される機会を永久に失い,メモリリークが発生します.そのため,new演算子を定義した場合には,たとえ明示的にそれを呼び出す機会がなくても,必ず対応するdelete演算子も定義する必要があります.

次に,delete演算子の振る舞いについても見てみましょう.

delete ptr;
  ↓
// delete演算子の振る舞いを示す擬似コード
if (ptr != 0)
{
    ptr->~Type();
}
operator delete(ptr);

これは,先ほどのnew演算子の擬似コードで生成したType型のオブジェクトを解体する場合を想定しています.new演算子の振る舞いに比べると非常に簡単ですが,ここで示したのは,あくまでも大域的なdelete演算子を呼び出す場合についてです.クラスのメンバー関数として,delete演算子が多重定義されていた場合には,少し振るまいが複雑になります.次のような状況を考えてみましょう.

class A
{
public:
    virtual ~A();
    static void* operator new(size_t size);
    static void operator delete(void* ptr);
};
class B : public A
{
public:
    virtual ~B();
    static void* operator new(size_t size);
    static void operator delete(void* ptr);
};

上に示したクラスAとBは継承関係にあり,両方ともnew演算子とdelete演算子を多重定義しています.この場合に,次のようにすると,はたしてどちらのクラスで定義したoperator delete関数が呼び出されるでしょうか?

A* ptr = new B;
delete ptr;

デストラクタは仮想関数になっているので,間違いなくBクラスのものが呼び出されます.しかし,operator delete関数は静的メンバー関数なので仮想関数にはなれません.それでも,operator delete関数は,それがあたかも仮想関数であるかのように,Bクラスのものが呼び出されます.実現方法は処理系に依存しますが,おそらくはoperator delete関数も仮想関数テーブルに含まれていると考えてよいでしょう.

3.6.2 operator new関数(割り付け関数)

これまでは,newやdeleteを演算子として呼び出した場合の振る舞いでしたが,今度は,operator new関数の内部ものぞいてみることにしましょう.

void* operator new(size_t size) throw(bad_alloc)
{
    if (size < 1)
        size = 1;
    void* ptr = malloc(size);
    while (ptr == 0)
    {
        new_handler nh = set_new_handler(0);
        set_new_handler(nh);
        if (nh == 0)
            throw bad_alloc();
        else
            (*nh)();
        ptr = malloc(size);
    }    return ptr;
}

operator new関数が,内部的にmalloc関数を呼び出すかどうかは規格では規定されていませんが,多くの処理系ではmalloc関数を使用しているようです.メモリブロックの割り付けに成功した場合は,そのままメモリブロックへのポインタを返しますが,失敗した場合は,set_new_handler関数で登録された処理関数を呼び出すようにできています.もし,最後にset_new_handler関数で登録した関数へのポインタが空ポインタの場合は処理関数を呼び出すことができませんので,bad_alloc例外を送出します.また,set_new_handler関数を一度も呼び出していない場合もbad_alloc例外が送出されます.処理関数を呼び出した場合には,メモリブロックの割り付けに成功するまで,何度でも再試行を続けます.

次は,nothrow_t型の参照引数を受け取る,例外を送出しない形式のnew演算子について見ていきたいと思います.例外処理は決して軽くはない機能ですので,できれば例外処理を使用しない軽量のnew演算子を使用したいと考える方も少なくないでしょう.しかし,この例外を送出しない形式のnew演算子は本当に軽量なのでしょうか?

void * operator new(size_t size, const nothrow_t&) throw()
{
    void * ptr;
    try
    {
        ptr = operator new(size);
    }
    catch (...)
    {
        ptr = 0;
    }
    return ptr;
}

これは例外を送出しない形式のnew演算子の定義例です.コードを見れば一目瞭然ですが,この形式のnew演算子は,例外処理を使用しないわけではなく,内部で発生した例外に,単にふたをしているにすぎません.例外を捉えるための処理が入っている分,通常のnew演算子より,成功時も失敗時も処理が重くなっています.この形式のnew演算子は,決して例外処理を使用しない軽量版ではなく,あくまでも古いコードの救済を目的として用意されたものなのです.軽量なnew演算子がほしいのであれば,標準ライブラリが提供しているものではなく,自分で必要なものを多重定義するしかありません.

位置指定形式(Placement Form)についても少し触れておきましょう.位置指定形式は,「配置構文」といういい方をされることもあります.この形式では,operator new演算子は何もせず,第2引数として渡されたポインタをそのまま返すだけです.new演算子の振る舞いを表す擬似コードをもう一度見ればわかるように,new演算子は,得られたメモリブロックに対して,コンストラクタを呼び出します.位置指定形式は,生のメモリブロックを,意味あるオブジェクトにするために,コンストラクタを呼び出すことが唯一の役割なのです.位置指定形式を使用して生成されたオブジェクトは,new演算子でメモリブロックが割り付けられたわけではありませんので,delete演算子を使って解体することができません.ですから,次のように,明示的にデストラクタを呼び出す必要があります.

// 境界調整には配慮していない
char mem[sizeof(Type)];
void* ptr = new(mem) Type;
// 明示的にデストラクタを呼び出す
ptr->~Type();

最後に,配列形式についてです.配列形式が,単一のオブジェクトを生成する形式と違うのは,コンストラクタやデストラクタを各要素について呼び出す必要がある点です.これについても,振る舞いを示すための擬似コードを書いてみます.

Type * ptr = new Type[10];
delete[] ptr;
  ↓
// new演算子の振る舞いを示す擬似コード
void* __temp = operator new[](sizeof(Type)*10
                            + sizeof(size_t));
Type* ptr;
if (__temp != 0)
{
    int i;
    *(size_t*)__temp = 10;  // 要素数を格納
    ptr = (Type*)((size_t*)__temp + 1);
    try
    {
        // コンストラクタを呼び出す
        // 実際にはこのような書き方はできない
        for (i = 0; i < 10; i++)
            (ptr + i)->Type(arg);
    }
    catch (...)
    {
        for (int j = 0; j < i; j++)
            (ptr + j)->~Type();
        operator delete[](__temp);
        throw;
    }
}
// delete演算子の振る舞いを示す擬似コード
if (ptr != 0)
{
    size_t* __temp2 = (size_t*)ptr - 1;
    int __n = (int)*__temp2;
    for (int i = 0; i < n; i++)
        (ptr + i)->~Type();
}
operator delete[](__temp2);

複数のオブジェクトを扱うために,コンストラクタやデストラクタを複数回呼び出さなければならなくなっています.ここで注目すべきなのは,new演算子が返す配列へのポインタが指すアドレスの直前に,配列の要素数が格納されている点です.実際にどうなるかは,処理系に依存する部分がかなりありますし,デストラクタを持たないオブジェクトでも要素数が格納されるかどうかは実装次第ですが,だいたいこのようなイメージになるかと思います.配列形式のnew演算子を多重定義するとき,何らかの理由で,サイズの計算を自分で行う必要がある場合には,配列の要素数を知るための若干のオーバーヘッドがあることを知っておく必要があります.

3.6.3 operator delete関数(解放関数)

operator new関数に比べて,operator delete関数の内部は非常に単純です.

void operator delete(void* ptr) throw()
{
    free(ptr);
}

たったこれだけです.配列形式も同じだと考えてください.

位置指定形式についても,次のように,単に第2引数以降は無視されます.

void operator delete(void* ptr, const nothrow_t&) throw()
{
    free(ptr);
}

なお,第2引数にvoid*型を受け取る形式だけは例外で,次のように,operator delete関数は何も行いません.これは,operator new(size_t size, void* ptr)がメモリブロックを割り付けるわけではなく,第2引数として与えられたポインタをそのまま返すためです.

void operator delete(void* ptr, void*) throw()
{
}

ところで,free関数を使ってメモリブロックを解放する場合はよいのですが,operator new関数やoperator delete関数をカスタマイズする場合には,空ポインタ(NULL)を渡されても誤動作しないように注意しなければなりません.free関数に空ポインタを渡した場合には,単に無視されることが言語規格上保証されていますが,他のアロケータを用いる場合には必ずしもそうではないからです.